package models.games;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;

import models.util.Indexes;
import core.User;

public class LuceRiflessa extends Game {

	public enum Direction {
		UP, RIGHT, DOWN, LEFT, STOP
	};

	private int currentRow, currentColumn;
	private Direction currentDirection;
	private int[][] matrix = new int[8][11];// vale 1 se è presente / , 2 se è
											// presente \
	private int bounces;// numero di rimbalzi: al massimo saranno 10 al livello
						// 9
	private int additionalSlashes;// stanghette aggiuntive pari alla metà di
									// bounces
	private Indexes[] slashesArray;// array con le posizioni delle stanghette
	private Set<Integer> excludedRows = new HashSet<Integer>();// rihe escluse
																// dal possibile
																// inserimento
																// di una
																// sbarretta
	private Set<Integer> excludedColumns = new HashSet<Integer>();// colonne
																	// escluse
																	// dal
																	// possibile
																	// inserimento
																	// di una
																	// sbarretta
	private List<Indexes> path = new LinkedList<Indexes>();// tutte le posizioni
															// percorse dalla
															// luce
	private Random random = new Random();

	public LuceRiflessa(User user) {
		super(user);
		initialize();
	}

	/**
	 * Fa avanzare la luce fin quando possibile
	 */
	private void calculatePath() {
		while (currentDirection != Direction.STOP) {
			progress();
			currentDirection = nextDirection(currentRow, currentColumn,
					currentDirection);

		}
	}

	public Direction getCurrentDirection() {
		return currentDirection;
	}

	/**
	 * @return currentColumn
	 */
	public int getCurrentRow() {
		return currentRow;
	}

	/**
	 * @return curcurrentColumnre
	 */
	public int getCurrentColumn() {
		return currentColumn;
	}

	/**
	 * @return matrix
	 */
	public int getValue(int x, int y) {
		if (x < 0 || x >= matrix.length || y < 0 || y >= matrix[0].length)
			throw new ArrayIndexOutOfBoundsException();
		return matrix[x][y];
	}

	public List<Indexes> getPath() {
		return path;
	}

	/**
	 * pone a 0 tutta la matrice e pulisce la lista path che contiene le celle
	 * percorse dalla luce
	 */
	public void reset() {
		for (int i = 0; i < matrix.length; i++)
			for (int j = 0; j < matrix[0].length; j++)
				matrix[i][j] = 0;
		path.clear();
	}

	/**
	 * Pone come posizione iniziale una cella a caso sul bordo della matrice e
	 * inizializza anche currentDirection
	 */
	private void startingPoint() {
		int casualSide = random.nextInt(4);
		switch (casualSide) {
		case 0:
			currentRow = 0;
			currentColumn = randomColumn();
			break;
		case 1:
			currentColumn = matrix[0].length - 1;
			currentRow = randomRow();
			break;
		case 2:
			currentRow = matrix.length - 1;
			currentColumn = randomColumn();
			break;
		case 3:
			currentColumn = 0;
			currentRow = randomRow();
			break;
		}
		currentDirection = initialDirection();
	}

	/**
	 * @return direzione random in accordo con la pozione inziale.Tutte le celle
	 *         tranne quelle agli angoli hanno una sola direzione possibile che
	 *         corrisponde all'entrata dal lato che delimita l'esterno della
	 *         matrice. Per gli angoli si calcola in maniera random.
	 */
	private Direction initialDirection() {
		if (currentRow == 0) {
			if (currentColumn != matrix[0].length - 1 && currentColumn != 0)
				return Direction.DOWN;
			else if (currentColumn == 0) {// angolo in alto a sinistra
				int dir = random.nextInt(2);
				return (dir == 0) ? Direction.DOWN : Direction.RIGHT;
			} else {// angolo in alto a destra
				int dir = random.nextInt(2);
				return (dir == 0) ? Direction.DOWN : Direction.LEFT;
			}
		}
		if (currentRow == matrix.length - 1) {
			if (currentColumn != matrix[0].length - 1 && currentColumn != 0)
				return Direction.UP;
			else if (currentColumn == 0) {// angolo in basso a sinistra
				int dir = random.nextInt(2);
				return (dir == 0) ? Direction.UP : Direction.RIGHT;
			} else {// angolo in basso a destra
				int dir = random.nextInt(2);
				return (dir == 0) ? Direction.UP : Direction.LEFT;
			}
		}
		if (currentColumn == 0) {
			return Direction.RIGHT;
		} else
			return Direction.LEFT;
	}

	/**
	 * @return una riga random
	 */
	private int randomRow() {
		return random.nextInt(matrix.length);
	}

	/**
	 * @return una colonna random
	 */
	private int randomColumn() {
		return random.nextInt(matrix[0].length);
	}

	public Indexes[] getBouncesArray() {
		return slashesArray;
	}

	public void refresh() {
		reset();
		initialize();
	}

	/**
	 * Inizializza la matrice creando un percorso con un numero di rimbalzi pari
	 * a bounces.
	 */
	private void initialize() {
		startingPoint();
		int row = -1;
		int column = -1;
		bounces = (this.getLevel() + 1);
		additionalSlashes = bounces / 2;
		slashesArray = new Indexes[bounces + additionalSlashes];
		// per semplicità solo righe interne, escluso il bordo
		if (currentRow == 0 || currentRow == matrix.length - 1) {// se parto dal
																	// lato
																	// superiore
																	// o
																	// inferiore
			if (currentDirection == Direction.UP
					|| currentDirection == Direction.DOWN) {// infatti negli
															// spigoli potebbe
															// non essere UP o
															// DOWN
				row = random.nextInt((matrix.length - 1) - 1) + 1;
				column = currentColumn;
				excludedColumns.add(column);
			} else {
				column = random.nextInt((matrix[0].length - 1) - 1) + 1;
				row = currentRow;
				excludedRows.add(row);
			}
		}
		// per semplicità solo colonne interne, escluso il bordo
		else {// lato sinistro o destro: la direzione per startingPoint() sarà
				// per forza o LEFT o RIGHT
			column = random.nextInt((matrix[0].length - 1) - 1) + 1;
			row = currentRow;
			excludedRows.add(row);
		}
		Direction dir = currentDirection;
		matrix[row][column] = bestValue(row, column, dir);
		slashesArray[0] = new Indexes(row, column);
		dir = nextDirection(row, column, dir);
		assignement(row, column, dir, 1);
		additionalSlashes();
		excludedRows.clear();
		excludedColumns.clear();

	}

	/**
	 * Aggiunge a caso un numero di sbarrette pari ad additionalSlashes.
	 * 
	 * @param excludedRows
	 *            : righe escluse perchè implicate nella traiettoria random
	 *            calcolata in initialize
	 * @param excludedColumns
	 *            : colonne escluse perchè implicate nella traiettoria random
	 *            calcolata in initialize
	 */
	private void additionalSlashes() {
		int effectiveRows = matrix.length - excludedRows.size();
		int effectiveColumns = matrix[0].length - excludedColumns.size();
		Indexes[] cartesianProduct = new Indexes[effectiveRows
				* effectiveColumns];
		int n = 0;
		for (int i = 0; i < matrix.length; i++)
			if (!excludedRows.contains(i))
				for (int j = 0; j < matrix[0].length; j++)
					if (!excludedColumns.contains(j))
						cartesianProduct[n++] = new Indexes(i, j);
		int add = additionalSlashes;
		int k = bounces; // k puntatore sull'array, quando il metodo è invocato
							// è stato riempito fino a bounces-1
		while (add > 0) {
			int ij = random.nextInt(n);
			Indexes indexes = cartesianProduct[ij];
			slashesArray[k++] = indexes;
			matrix[indexes.getI()][indexes.getJ()] = bestValue(indexes.getI(),
					indexes.getJ(), Direction.STOP);
			// scambia con l'ultimo
			Indexes tmp = cartesianProduct[ij];
			cartesianProduct[ij] = cartesianProduct[n - 1];
			cartesianProduct[--n] = tmp;
			add--;
		}

	}

	/**
	 * @param row
	 * @param column
	 * @param dir
	 * @return una sbarretta ( 1 = / , 2 = \ ) a caso se dir = STOP, altrimenti
	 *         ritorna la sbarretta che fa rimbalzare la luce nella direzione in
	 *         cui c'è più spazio rispetto alla posizione corrente
	 */
	private int bestValue(int row, int column, Direction dir) {
		switch (dir) {
		case UP:
			int diff = matrix[0].length - (column + 1);
			if (diff >= (matrix[0].length) / 2)
				return 1;
			else
				return 2;
		case DOWN:
			int diff1 = matrix[0].length - (column + 1);
			if (diff1 >= (matrix[0].length) / 2)
				return 2;
			else
				return 1;
		case LEFT:
			int diff2 = matrix.length - (row + 1);
			if (diff2 >= (matrix.length) / 2)
				return 1;
			else
				return 2;
		case RIGHT:
			int diff3 = matrix.length - (row + 1);
			if (diff3 >= (matrix.length) / 2)
				return 2;
			else
				return 1;
		default:
			return random.nextInt(2) + 1;
		}
	}

	/**
	 * @param row
	 * @param column
	 * @param previousDir
	 * @return la prossima direzione tenendo conto del valore nella cella
	 *         [row][column]. Ritorna STOP se lastPossiblePosition() è true.
	 *         Solleva un'eccezione se previousDir è STOP.
	 */
	private Direction nextDirection(int row, int column, Direction previousDir) {
		int value = matrix[row][column];
		Direction dir = previousDir;
		switch (value) {
		case 1:
			switch (dir) {
			case UP:
				dir = Direction.RIGHT;
				break;
			case RIGHT:
				dir = Direction.UP;
				break;
			case DOWN:
				dir = Direction.LEFT;
				break;
			case LEFT:
				dir = Direction.DOWN;
				break;
			default:
				throw new IllegalStateException();
			}
			break;
		case 2:
			switch (dir) {
			case UP:
				dir = Direction.LEFT;
				break;
			case RIGHT:
				dir = Direction.DOWN;
				break;
			case DOWN:
				dir = Direction.RIGHT;
				break;
			case LEFT:
				dir = Direction.UP;
				break;
			default:
				throw new IllegalStateException();
			}
			break;
		default:
			break;
		}
		if (lastPossiblePosition(dir, row, column))
			dir = Direction.STOP;
		return dir;
	}

	/**
	 * Nota: Quando questo metodo viene invocato la prossima posizione è già
	 * stata calcolata.
	 * 
	 * @return true se il prossimo spostamento della luce ( progress() ) ne
	 *         comporta l'uscita fuori dalla matrice
	 */
	private boolean lastPossiblePosition(Direction dir, int row, int column) {
		if (!onTheBorder())
			return false;
		switch (dir) {
		case UP:
			return row == 0;
		case RIGHT:
			return column == matrix[0].length - 1;
		case DOWN:
			return row == matrix.length - 1;
		case LEFT:
			return column == 0;
		default:
			throw new IllegalStateException();
		}
	}

	/**
	 * @return true se la posizione corrente si trova sul bordo
	 */
	private boolean onTheBorder() {
		return (currentRow == 0 || currentRow == matrix.length - 1
				|| currentColumn == 0 || currentColumn == matrix[0].length - 1);
	}

	/**
	 * avanza la posizione corrente della luce in base alla direzione corrente
	 */
	private void progress() {
		path.add(new Indexes(currentRow, currentColumn));
		switch (currentDirection) {
		case UP:
			currentRow--;
			path.add(new Indexes(currentRow, currentColumn));
			break;
		case RIGHT:
			currentColumn++;
			path.add(new Indexes(currentRow, currentColumn));
			break;
		case DOWN:
			currentRow++;
			path.add(new Indexes(currentRow, currentColumn));
			break;
		case LEFT:
			currentColumn--;
			path.add(new Indexes(currentRow, currentColumn));
			break;
		default:
			throw new IllegalStateException();
		}

	}

	public boolean verify(int i, int j) {
		calculatePath();
		boolean won = currentRow == i && currentColumn == j;
		if (won && getLevel() < 9)
			this.setLevel(getLevel() + 1);
		else if (getLevel() > 0)
			this.setLevel(getLevel() - 1);
		return won;
	}

	/**
	 * invocato quando currentRow e currentColumn rappresentano la posizione
	 * iniziale
	 * 
	 * @return true se è presente un ciclo infinito
	 */
	private boolean cicleOrMultiBouncing(int simulationRow,
			int simulationColumn, Direction dir, int value) {
		boolean cicle = false;
		Set<Indexes> touchedSlashes = new HashSet<Indexes>();
		matrix[simulationRow][simulationColumn] = value;// simulo inserimento in
														// cella
		// parto dalla posizione iniziale
		int row = currentRow;
		int column = currentColumn;
		while (dir != Direction.STOP) {
			if (matrix[row][column] != 0) {
				Indexes ind = new Indexes(row, column);
				if (touchedSlashes.contains(ind)) {
					cicle = true;
					break;
				} else
					touchedSlashes.add(ind);
			}
			switch (dir) {
			case UP:
				row--;
				break;
			case RIGHT:
				column++;
				break;
			case DOWN:
				row++;
				break;
			case LEFT:
				column--;
				break;
			default:
				throw new IllegalStateException();
			}
			if (row < 0 || row == matrix.length || column < 0
					|| column == matrix[0].length)
				return cicle;
			dir = nextDirection(row, column, dir);
		}
		matrix[simulationRow][simulationColumn] = 0;// finita la simulazione
													// tutto torna come prima
		return cicle;
	}

	/**
	 * @param row
	 *            : riga del precedente inserimento di sbarretta
	 * @param column
	 *            : colonna del precedente inserimento di sbarretta
	 * @param dir
	 *            : direzione in cui si sta ora muovendo la luce
	 * @param currentBounces
	 *            : rimbalzo attuale
	 * @return
	 */
	private int assignement(int row, int column, Direction dir,
			int currentBounces) {
		// Escludo dalle future scelte la riga o colonna in cui ora si sta
		// muovendo la luce
		if (dir == Direction.LEFT || dir == Direction.RIGHT) {
			excludedRows.add(row);
		} else {
			excludedColumns.add(column);
		}
		if (currentBounces == bounces)
			return bounces;
		int[] choosings = possibleChoosings(row, column, dir);
		if (choosings.length != 0) {
			int dim = choosings.length;
			for (int i = 0; i < choosings.length; i++) {
				int current = random.nextInt(dim);
				// scambia con l'ultimo per non ritornarci
				dim--;
				int tmp = choosings[current];
				choosings[current] = choosings[dim];
				choosings[dim] = tmp;
				//
				if (dir == Direction.LEFT || dir == Direction.RIGHT) {
					column = choosings[current];
				} else {
					row = choosings[current];
				}
				int value = bestValue(row, column, dir);
				if (!cicleOrMultiBouncing(row, column, dir, value)) {
					slashesArray[currentBounces] = new Indexes(row, column);
					matrix[row][column] = value;
					currentBounces++;

					Direction nextDir = nextDirection(row, column, dir);
					currentBounces = assignement(row, column, nextDir,
							currentBounces);

					if (currentBounces == bounces)
						return bounces;
					matrix[row][column] = 0;
					// CurrentBounces è già diminuito per la chiamata ricorsiva
					slashesArray[currentBounces] = null;
				}
			}
		}
		// se arriva qui vuol dire che le ha provate tutte, ma non c'è stato
		// verso
		if (dir == Direction.LEFT || dir == Direction.RIGHT) {
			excludedRows.remove(row);
		} else {
			excludedColumns.remove(column);
		}
		return currentBounces - 1;
	}

	private int[] possibleChoosings(int row, int column, Direction dir) {
		if (dir == Direction.STOP)
			throw new IllegalArgumentException("STOP non ammesso");
		int[] minMax = minMaxValue(row, column, dir);
		int min = minMax[0];
		int max = minMax[1];
		int[] choosings = new int[max - min + 1];
		int k = 0;
		if (dir == Direction.LEFT || dir == Direction.RIGHT) {
			for (int i = min; i <= max; i++)
				if (!excludedColumns.contains(i))
					choosings[k++] = i;
		} else {
			for (int i = min; i <= max; i++)
				if (!excludedRows.contains(i))
					choosings[k++] = i;
		}
		int[] v = Arrays.copyOf(choosings, k);
		return v;
	}

	private int[] minMaxValue(int row, int column, Direction dir) {
		int[] v = new int[2];
		switch (dir) {
		case LEFT:
			v[0] = 1;
			v[1] = column - 1;
			break;
		case RIGHT:
			v[0] = column + 1;
			v[1] = matrix[0].length - 2;
			break;
		case DOWN:
			v[0] = row + 1;
			v[1] = matrix.length - 2;
			break;
		case UP:
			v[0] = 1;
			v[1] = row - 1;
			break;
		default:
			throw new IllegalArgumentException(dir.toString());
		}
		return v;
	}

	@Override
	public int getGameKey() {
		return 4;
	}

}
